Skip to content

Add OCI container annotation support#55415

Open
jetersen wants to merge 6 commits into
dotnet:mainfrom
jetersen:feat/add-container-annotations
Open

Add OCI container annotation support#55415
jetersen wants to merge 6 commits into
dotnet:mainfrom
jetersen:feat/add-container-annotations

Conversation

@jetersen

Copy link
Copy Markdown
Contributor

Summary

  • add a ContainerAnnotation MSBuild item for OCI image metadata
  • write annotations to single-platform OCI manifests and top-level multi-architecture OCI indexes
  • provide opt-in conventional annotations from existing container and Source Link metadata
  • automatically select OCI when annotations are requested, while rejecting an explicitly selected Docker format

Motivation

OCI annotations describe the published manifest or image index itself. Registry consumers cannot reliably obtain this metadata from configuration labels, particularly for multi-platform images where the tag resolves to an image index.

Generated annotations are opt-in to preserve the existing behavior where output format follows the base image. Architecture-specific base image and .NET toolset metadata remain labels and are not copied to the top-level index.

Validation

  • Microsoft.NET.Build.Containers unit tests: 288 passed, 7 existing skips
  • focused manifest and image-index annotation tests pass
  • integration test project compiles with target coverage for generated metadata and image-format compatibility
  • git diff --check passes

Running the integration tests requires a built redist SDK; the attempted redist build was blocked by a pre-existing missing project.assets.json in the Blazor WebAssembly tool chain after the Containers projects compiled and packed.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@jetersen
jetersen marked this pull request as ready for review July 22, 2026 17:46
@jetersen
jetersen requested a review from a team as a code owner July 22, 2026 17:46
Copilot AI review requested due to automatic review settings July 22, 2026 17:46
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
2 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class OCI manifest/index annotation support to the .NET SDK container publishing pipeline, enabling registry consumers to read metadata from image manifests and multi-arch indexes (not just config labels).

Changes:

  • Introduces a ContainerAnnotation MSBuild item and wires it through container build/publish tasks.
  • Emits annotations into single-platform OCI manifests and top-level multi-arch OCI indexes, with opt-in conventional annotations derived from existing package/source metadata.
  • Enforces/auto-selects OCI output when annotations are present and rejects explicitly-selected Docker format.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/Microsoft.NET.Build.Containers.UnitTests/ImageIndexGeneratorTests.cs Adds unit coverage for index-level OCI annotations and Docker manifest list behavior.
test/Microsoft.NET.Build.Containers.UnitTests/ImageBuilderTests.cs Adds unit coverage for manifest-level OCI annotations.
test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs Validates conventional annotation generation and the “annotations require OCI” enforcement at the MSBuild layer.
src/Containers/packaging/build/Microsoft.NET.Build.Containers.targets Defines annotation-related properties/items, generates conventional annotations, auto-selects OCI, and errors on Docker format when annotations exist.
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.Interface.cs Exposes Annotations task parameter for single-image creation.
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateNewImage.cs Plumbs MSBuild ContainerAnnotation items into ImageBuilder.
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.Interface.cs Exposes Annotations task parameter for multi-arch index creation.
src/Containers/Microsoft.NET.Build.Containers/Tasks/CreateImageIndex.cs Collects annotations and passes them into index generation.
src/Containers/Microsoft.NET.Build.Containers/PublicAPI/net11.0/PublicAPI.Unshipped.txt Declares newly added public API surface for annotations on tasks/manifests/indexes.
src/Containers/Microsoft.NET.Build.Containers/ManifestV2.cs Adds Annotations to OCI manifest model with null-when-empty serialization behavior.
src/Containers/Microsoft.NET.Build.Containers/ManifestListV2.cs Adds index/list-level annotations properties to the manifest list and OCI index models.
src/Containers/Microsoft.NET.Build.Containers/ImageIndexGenerator.cs Extends index generation APIs to accept optional annotations and serialize them into OCI indexes.
src/Containers/Microsoft.NET.Build.Containers/ImageBuilder.cs Adds annotation capture and emission for OCI manifests.

@jetersen

Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree

@baronfel baronfel added the Area-Containers Related to dotnet SDK containers functionality label Jul 22, 2026
…annotations

# Conflicts:
#	test/Microsoft.NET.Build.Containers.IntegrationTests/TargetsTests.cs
@jetersen

Copy link
Copy Markdown
Contributor Author

@baronfel I think this is useful. Annotations on manifest are used security tooling and inventory tooling.
Yes, we could dig through the manifest and find the individual image and locate the image labels but not all tooling does that.

If you want me to change it from opt-in to default for OCI image type with a opt-out option, just let me know :)

@baronfel

Copy link
Copy Markdown
Member

Thanks for the ping - I saw this and was thinking about how I'd like the concept to be expressed. Usually we'd do the design-side of things over issue/discussion before an implementation to help hammer out some of those details, but happy to iterate here.

My main questions are things like:

  • which formats are allowed to have annotations
  • what's the relationship between Index-level annotations and child-level annotations (do they flow, are they distinct, if they are distinct what's a nice way to make that clear/possible for consumers of the SDK tech)
  • are there different conventions for annotations that we should take into account?
  • for common container tooling (scopeo, and the like) and common registries, are there any expectations for annotations?

So a split of questions related to physical layout of the annotations, the UX of defining them, and then opportunities for being better OCI citizens by pre-seeding specific annotations. We do a lot of this with Labels already, but Labels were more 'settled' IMO.

@jetersen

Copy link
Copy Markdown
Contributor Author

Yes, I went straight to PR thinking my issue at hand was simply enough but after having AI dig into various tools then ya answer is more complex.

Can read the TL;DR and jump to proposed changes.

TL;DR: OCI manifest, index, and descriptor annotations are independent, and existing tools consume different scopes. I propose
retaining config labels while giving ContainerAnnotation explicit manifest/index scope, with architecture-neutral generated annotations emitted at both scopes and platform-specific metadata kept on manifests.

AI Research 🤖

I did some research across the OCI specifications, Buildx/Podman/Skopeo, registries, and consumers including Kargo, Flux, Argo CD, Harbor, Renovate, Syft, Trivy, and Crane.

Formats and physical placement

OCI defines annotations independently on several object types:

Scope Meaning
Image manifest Metadata about a single-platform image or OCI artifact
Image index Metadata about a multi-platform/index object
Descriptor Metadata about a referenced manifest, config, layer, subject, or other object
Image config Does not have an annotations field; config.Labels is a separate metadata map

There is no OCI inheritance or merging between these locations. Index annotations do not flow to child manifests, child annotations do not flow to their index, and config labels do not become annotations.

Docker schema-2 manifests and manifest lists do not define annotations. My conclusion is that Microsoft.NET.Build.Containers should emit annotations only for OCI output. Requesting annotations should select OCI when the format is unspecified and produce an error when Docker is explicitly requested. The public container task APIs should enforce this too rather than silently discarding annotations.

Existing consumers

The public tooling I found has varied—but concrete—scope expectations:

Tool Metadata consumed Implication
Kargo Manifest annotations for single-platform tags; top-level index annotations for multi-platform tags Recommends annotating both index,manifest; does not generally fall back to labels for source/revision
Flux OCIRepository Selected child manifest annotations Preserves arbitrary keys in status and surfaces source/revision; index-only annotations and config labels are invisible
Argo CD native OCI sources Root manifest annotations Reads standard metadata such as created, authors, documentation, source, version, and description; no index or label fallback
Harbor Root manifest and index annotations Copies arbitrary keys into its artifact model and exposes them in the registry UI
Renovate Root manifest/index annotations and selected descriptor annotations Uses source, revision, and URL for package metadata
Syft Root image-manifest annotations Preserves arbitrary annotations separately from config labels
Trivy Manifest and layer-descriptor annotations for OCI artifacts Uses manifest version for policy bundles and layer title for artifact filenames
Crane rebase Image-manifest base.name and base.digest These values are inherently platform-manifest metadata; Crane rejects indexes

Config labels remain independently important. Argo CD Image Updater and Dependabot read standard metadata from config labels, and normal Skopeo inspection primarily exposes labels. I therefore do not think annotations should replace the existing labels.

Kargo is the motivating concrete example here. Its behavior is OCI-compliant, but it is an application convention rather than an OCI-required placement rule. For a multi-platform tag it reads the root index, while Flux and some other consumers resolve the index and read a child manifest. Supporting both requires deliberate duplication because nothing flows automatically.

The public tools are also not exhaustive. OCI intentionally permits arbitrary namespaced annotations, and internal inventory, policy, provenance, promotion, or deployment systems may depend on organization-specific keys. Microsoft.NET.Build.Containers should preserve arbitrary user annotations rather than limit them to keys recognized by known public tools.

Annotation conventions

OCI defines standard org.opencontainers.image.* keys including created, authors, url, documentation, source, version, revision, vendor, licenses, title, description, base.name, and base.digest.

My proposed conventional placement would be:

Metadata Default generated scope
Source, revision, URL, version, created, description, authors, vendor, licenses Manifest and Index
Immediate base name/digest Manifest only
Architecture/runtime-specific metadata Manifest only
Arbitrary user metadata Explicitly selected by the user

The container tooling should respect the defined formats—for example RFC 3339 for created and SPDX expressions for licenses. org.opencontainers.image.ref.name has special image-layout descriptor semantics and should not be generated as a general registry-manifest annotation.

Proposed MSBuild UX

The current ContainerAnnotation behavior is too implicit: the same item effectively means “manifest for a single-platform image, index for a multi-platform image.” That obscures OCI’s separate scopes.

My preference is to use one annotation item type, ContainerAnnotation, separate from the existing ContainerLabel item, and add explicit scope metadata to each annotation:

<ItemGroup>
  <ContainerAnnotation Include="org.opencontainers.image.source"
                       Value="$(RepositoryUrl)"
                       Scope="Manifest,Index" />

  <ContainerAnnotation Include="com.example.pipeline.id"
                       Value="$(PipelineId)"
                       Scope="Index" />
</ItemGroup>

Proposed semantics:

  • Manifest applies to the single image manifest or each child image manifest.
  • Index applies to the top-level multi-platform index when one exists.
  • Manifest,Index deliberately duplicates the value.
  • User-provided annotations default to Manifest, matching Buildx’s default.
  • Architecture-neutral annotations generated by ContainerGenerateAnnotations use Manifest,Index.
  • Platform-specific generated annotations use Manifest.
  • Descriptor scopes and platform selectors can be added later when we have concrete scenarios for them.

This follows the model exposed by Buildx, which distinguishes manifest, index, manifest-descriptor, and index-descriptor, without requiring the initial implementation to support every descriptor scope.

I would keep annotation generation opt-in initially because enabling it changes the output media type and manifest digests, and not every local image-store path can preserve OCI annotations. Local Docker/Podman paths that require conversion through Docker archives should either preserve OCI output or clearly reject/document annotations rather than silently losing them.

Based on this research, I think the PR should be revised around explicit manifest/index scope, ensure generated annotations are forwarded into multi-RID child manifests, retain equivalent config labels, and add end-to-end coverage for both the root index and child manifests.

Does that scope model align with how you would like the concept expressed?

@jetersen

Copy link
Copy Markdown
Contributor Author

Went ahead and implemented the scoped OCI container annotations, can always drop the commit or rework it.

@jetersen

jetersen commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Added the manifest specific OCI annotations. For reference here are the outputted annotation:

Multi-RID index

{
  "org.opencontainers.image.authors": ".NET SDK team",
  "org.opencontainers.image.created": "2026-07-24T05:12:02.6163113Z",
  "org.opencontainers.image.description": "Annotation smoke test",
  "org.opencontainers.image.documentation": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.licenses": "MIT",
  "org.opencontainers.image.revision": "abcdef123456",
  "org.opencontainers.image.source": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.title": "Container annotation smoke test",
  "org.opencontainers.image.url": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.vendor": "Microsoft",
  "org.opencontainers.image.version": "1.2.3"
}

Multi-RID child manifest: linux/amd64

{
  "net.dot.runtime.majorminor": "11.0",
  "net.dot.sdk.version": "11.0.100-preview.5.26302.115",
  "org.opencontainers.image.authors": ".NET SDK team",
  "org.opencontainers.image.base.digest": "sha256:ca17638b4cbab2063771e8943dbe887ea6c0ae00c5fede3498d5938758c85dfe",
  "org.opencontainers.image.base.name": "mcr.microsoft.com/dotnet/runtime:10.0",
  "org.opencontainers.image.created": "2026-07-24T05:12:02.6163113Z",
  "org.opencontainers.image.description": "Annotation smoke test",
  "org.opencontainers.image.documentation": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.licenses": "MIT",
  "org.opencontainers.image.revision": "abcdef123456",
  "org.opencontainers.image.source": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.title": "Container annotation smoke test",
  "org.opencontainers.image.url": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.vendor": "Microsoft",
  "org.opencontainers.image.version": "1.2.3"
}

Multi-RID child manifest: linux/arm64

{
  "net.dot.runtime.majorminor": "11.0",
  "net.dot.sdk.version": "11.0.100-preview.5.26302.115",
  "org.opencontainers.image.authors": ".NET SDK team",
  "org.opencontainers.image.base.digest": "sha256:954f9a563ea96979d219f3c78fccea104b5f3326e394bcdf9274032f3b73ddd1",
  "org.opencontainers.image.base.name": "mcr.microsoft.com/dotnet/runtime:10.0",
  "org.opencontainers.image.created": "2026-07-24T05:12:02.6163113Z",
  "org.opencontainers.image.description": "Annotation smoke test",
  "org.opencontainers.image.documentation": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.licenses": "MIT",
  "org.opencontainers.image.revision": "abcdef123456",
  "org.opencontainers.image.source": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.title": "Container annotation smoke test",
  "org.opencontainers.image.url": "https://github.com/dotnet/sdk",
  "org.opencontainers.image.vendor": "Microsoft",
  "org.opencontainers.image.version": "1.2.3"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-Containers Related to dotnet SDK containers functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants